#!/usr/bin/env python
#
# net.py
#
# Version : 0.7
# Date : 20080521
#
# Author : Bernard Czenkusz
# Email  : bernie@skipole.co.uk

#
# net.py - holds network and configuration data, run as part of skipolemonitor
# Copyright (c) 2007 Bernard Czenkusz
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#

"""Reads and writes the network and configuration files

Network information is held in a dictionary of groups and hosts, called devices{}
functions are defined allowing other modules to read and write to this dictionary

Application configuration items are available in the global dictionary net_cfg{}
"""

import cPickle
import os
import sys
import shutil
import ConfigParser
import validate
import csv


# This global variable is used to inform threads to shutdown on program exit
PROGRAM_SHUTDOWN = False


# Initial values set in devices{} is the All Alerts page and the Top page
devices={0:[1,"0",0,"All Alerts","All Hosts showing alerts",0,0,0,"0"],
         1:[0,"0",0,"Top","Skipole Monitor",0,0,0,"0"]}

# devices dictionary description,
# Each key is an incrementing number, 0, 1 etc ...
# Each value is a list of items, these are given in order below:

# List Index

#   0   Identifier of the parent group (integer)
#   1   '0' for a group, or an address such as '192.168.1.1' for a host (string)
#   2   Status; 0:white, 1:green, 2:yellow, 3:red (integer)
#   3   Short name (string)
#   4   Longer description (string)
#   5   Email Alert enabled, 0:disabled 1:enabled
#   6   Previous status - i.e status at the last poll (integer)
#   7   future (integer)
#   8   future (string)


# The first value (key 0) of the dictionary is always the 'All Alerts' page, and its
# 'parent identifier' is the identifier of the last item in the dictionary
#
# so devices[0][0] will give the key of the last item in the dictionary
#
# The second value (key 1) of the dictionary is always the 'Top' page, and its
# 'parent identifier' is zero
#
# Third and subsequent values will follow the convention above, the third value must
# have the top page (1) as its identifier, the 'All Alerts' page (0) cannot be used as
# the parent of any subsequent page or host.

# The devices dictionary is not used directly by external modules, instead they
# call the functions defined below, which set and read dictionary values
#
# The functions use the following parameters, which are defined here:

PARENT=0
ADDRESS=1
STATUS=2
NAME=3
DESCRIPTION=4
EMAIL=5
OLDSTATUS=6

# The directory where the script is kept
SCRIPTDIRECTORY=os.path.abspath(os.path.dirname(sys.argv[0]))

# The name of the file where the program configuration is kept
CONFIG_FILE="monitor.cfg"

# The folder where the configuration file and language file are kept
CONFIG_FOLDER=SCRIPTDIRECTORY

# Host stats : [Total number of hosts, number of red hosts, yellow hosts, green hosts]
HOST_STATS=[0,0,0,0]

# Flag that a network reload is in progress, i.e. while NET_RELOAD is 1, don't bother
# polling the network
NET_RELOAD=0

# Request a network html refresh, i.e. set HTML_REFRESH to 1 to request a refresh
HTML_REFRESH=0 

# Application configuration items are available in the global dictionary
net_cfg = {}

# Web server is enabled
net_cfg["WEB_SERVER_ENABLED"]="1"

# Define root directory of html files
net_cfg["HTTPROOT"]=os.path.join(SCRIPTDIRECTORY,'httproot')

# The name of the file where the current working network is kept as a pickled dictionary
net_cfg["NETWORK_DATA_FILE"]="network.dat"

# The folder where the current network data file is kept
net_cfg["NETWORK_DATA_FOLDER"]=os.path.join(SCRIPTDIRECTORY,'data')

# The default folder where saved network data files are kept
net_cfg["EXTERNAL_NETWORKS"]=os.path.join(SCRIPTDIRECTORY,'external_networks')

# The folder where images are kept
net_cfg["IMAGES_FOLDER"]=os.path.join(SCRIPTDIRECTORY,'images1')

# define the port the web server listens at
net_cfg["PORT"]="8000"

# The browser refresh time in seconds set in the html headers
net_cfg["BROWSER_REFRESH"]="90"

# The time between polls in seconds, normally 300 (5 minutes)
net_cfg["POLL_INTERVAL"]="300"
# It can only be one of:
# 60  -  1 minute
# 120 -  2 minutes
# 300 -  5 minutes
# 600 -  10 minutes

# The number of polls recorded in the host logs
net_cfg["LOG_RECORDS"]="50"

# Email alerts globally enabled
net_cfg["EMAIL_ENABLED"]="0"

# Email max rate (max number of emails sent per 24 hours)
net_cfg["EMAIL_MAX_RATE"]="50"

# Email source address
net_cfg["EMAIL_SOURCE"]="no-reply@localhost"

# Email subject
net_cfg["EMAIL_SUBJECT"]="Alert from Skipole Monitor"

# Email destination addresses (semicolon separated)
net_cfg["EMAIL_DESTINATION"]="skipolemonitor@localhost"

# Email smtp server
net_cfg["SMTP_SERVER"]="not.set"

# Group syslog alerts enabled
net_cfg["GROUP_SYSLOG_ENABLED"]="0"

# Host syslog alerts enabled
net_cfg["HOST_SYSLOG_ENABLED"]="0"

# Syslog server
net_cfg["SYSLOG_SERVER"]="not.set"

# Syslog UDP Port
net_cfg["SYSLOG_PORT"]="514"

# Syslog Facility
net_cfg["SYSLOG_FACILITY"]="1"


def start_network():
    """Loads the program config parameters, creates necessary folders
       and loads the network."""
    # Return 0 if success, an exit code on failure
    # with an appropriate error message
    global net_cfg
    global SCRIPTDIRECTORY
    #
    # Get the program config data
    if not getconfig(): return 4, "Could not access the config file"
    #
    # The HTTPROOT folder must exist
    if not os.path.exists(net_cfg["HTTPROOT"]): return 5, "The HTTP root folder does not exist"
    #
    # If the data folder which holds logs and ping history databases
    # does not exist then it must be created
    if not os.path.exists(net_cfg["NETWORK_DATA_FOLDER"]):
        os.mkdir(net_cfg["NETWORK_DATA_FOLDER"])
    #
    # In previous versions the data file was held in the scriptdirectory, search for
    # it there, and if it exists, move it
    old_filepath=os.path.join(SCRIPTDIRECTORY, net_cfg["NETWORK_DATA_FILE"])
    filepath=os.path.join(net_cfg["NETWORK_DATA_FOLDER"], net_cfg["NETWORK_DATA_FILE"])
    if os.path.exists(old_filepath):
        shutil.move(old_filepath, filepath)
    #
    # Get the network
    if not load_network(): return 6, "Could not read the network data"
    #
    return 0, "All OK"


def del_remake_data():
    """Delete and remake the data folder"""
    if os.path.exists(net_cfg["NETWORK_DATA_FOLDER"]):
        del_dir_contents(net_cfg["NETWORK_DATA_FOLDER"])
    else:
        os.mkdir(net_cfg["NETWORK_DATA_FOLDER"])
    #
    # And write network.dat to the folder
    if not save_network(): return 0
    return 1


def del_dir_contents(dirpath):
    """Deletes the contents of a directory, but not the directory itself
       returns True on success, False on failure"""
    #
    # Check the dirpath is a directory
    if not os.path.isdir(dirpath):return False
    #
    # Get directory listing
    dir_list = os.listdir(dirpath)
    for x in dir_list:
        filepath=os.path.join(dirpath,x)
        if os.path.isdir(filepath):
            shutil.rmtree(filepath,1)
        else:
            os.remove(filepath)
    return True
        

def getconfig():
    """Read the CONFIG_FILE, and if it does not exist, or parts do not exist, substitute defaults instead"""
    global net_cfg
    global CONFIG_FOLDER
    global CONFIG_FILE
    # Now create a ConfigParser object, to read the config file
    cfg=ConfigParser.ConfigParser()
    filepath=os.path.join(CONFIG_FOLDER, CONFIG_FILE)
    if os.path.exists(filepath):
        # read it
        try:
            fp=open(filepath, "rb")
            cfg.readfp(fp)
            fp.close()
        except:
            return 0
        # Read each parameter in turn
        #
        # Web service options
        if cfg.has_option("web", "web_server_enabled"):
            net_cfg["WEB_SERVER_ENABLED"]=cfg.get("web", "web_server_enabled")
        if cfg.has_option("web", "httproot_folder"):
            net_cfg["HTTPROOT"]=cfg.get("web", "httproot_folder")
        if cfg.has_option("web", "images_folder"):
            net_cfg["IMAGES_FOLDER"]=cfg.get("web", "images_folder")
        if cfg.has_option("web", "port"):
            net_cfg["PORT"]=cfg.get("web", "port")
        if cfg.has_option("web", "browser_refresh"):
            net_cfg["BROWSER_REFRESH"]=cfg.get("web", "browser_refresh")
        if cfg.has_option("web", "log_records"):
            net_cfg["LOG_RECORDS"]=cfg.get("web", "log_records")
        #
        # network data options
        if cfg.has_option("net", "current_network_file"):
            net_cfg["NETWORK_DATA_FILE"]=cfg.get("net", "current_network_file")
        #
        if cfg.has_option("net", "current_network_folder"):
            net_cfg["NETWORK_DATA_FOLDER"]=cfg.get("net", "current_network_folder")
        #
        if cfg.has_option("net", "external_networks_folder"):
            net_cfg["EXTERNAL_NETWORKS"]=cfg.get("net", "external_networks_folder")
        #
        # Only certain poll intervals are allowed
        if cfg.has_option("net", "poll_interval"):
            poll_interval=cfg.get("net", "poll_interval")
            net_cfg["POLL_INTERVAL"]="300"
            if poll_interval == "60":
                net_cfg["POLL_INTERVAL"]="60"
            if poll_interval == "120":
                net_cfg["POLL_INTERVAL"]="120"
            if poll_interval == "600":
                net_cfg["POLL_INTERVAL"]="600"
        #
        # email options
        if cfg.has_option("email", "email_enabled"):
            net_cfg["EMAIL_ENABLED"]=cfg.get("email", "email_enabled")
        #
        if cfg.has_option("email", "email_max_rate"):
            text=cfg.get("email", "email_max_rate")
            if validate.is_text_integer(text, 1, 300):
                net_cfg["EMAIL_MAX_RATE"]=text
        #
        if cfg.has_option("email", "email_source"):
            net_cfg["EMAIL_SOURCE"]=cfg.get("email", "email_source")
        if cfg.has_option("email", "email_destination"):
            net_cfg["EMAIL_DESTINATION"]=cfg.get("email", "email_destination")
        if cfg.has_option("email", "email_subject"):
            net_cfg["EMAIL_SUBJECT"]=cfg.get("email", "email_subject")
        if cfg.has_option("email", "smtp_server"):
            net_cfg["SMTP_SERVER"]=cfg.get("email", "smtp_server")
        #
        # syslog options
        if cfg.has_option("syslog", "group_syslog_enabled"):
            net_cfg["GROUP_SYSLOG_ENABLED"]=cfg.get("syslog", "group_syslog_enabled")
        if cfg.has_option("syslog", "host_syslog_enabled"):
            net_cfg["HOST_SYSLOG_ENABLED"]=cfg.get("syslog", "host_syslog_enabled")
        if cfg.has_option("syslog", "syslog_server"):
            net_cfg["SYSLOG_SERVER"]=cfg.get("syslog", "syslog_server")
        if cfg.has_option("syslog", "syslog_port"):
            net_cfg["SYSLOG_PORT"]=cfg.get("syslog", "syslog_port")
        if cfg.has_option("syslog", "syslog_facility"):
            net_cfg["SYSLOG_FACILITY"]=cfg.get("syslog", "syslog_facility")
        #
    # write out the config file
    if not writeconfigtofile():
        return 0
    return 1

def writeconfigtofile():
    """Save the dictionary of setup values to CONFIG_FILE"""
    global net_cfg
    global CONFIG_FOLDER
    global CONFIG_FILE
    cfg=ConfigParser.ConfigParser()
    cfg.add_section("web")
    cfg.add_section("net")
    cfg.add_section("email")
    cfg.add_section("syslog")
    cfg.set("web", "web_server_enabled", net_cfg["WEB_SERVER_ENABLED"])
    cfg.set("web", "httproot_folder", net_cfg["HTTPROOT"])
    cfg.set("web", "images_folder", net_cfg["IMAGES_FOLDER"])
    cfg.set("web", "port", net_cfg["PORT"])
    cfg.set("web", "browser_refresh", net_cfg["BROWSER_REFRESH"])
    cfg.set("web", "log_records", net_cfg["LOG_RECORDS"])
    cfg.set("net", "current_network_file", net_cfg["NETWORK_DATA_FILE"])
    cfg.set("net", "current_network_folder", net_cfg["NETWORK_DATA_FOLDER"])
    cfg.set("net", "external_networks_folder", net_cfg["EXTERNAL_NETWORKS"])
    cfg.set("net", "poll_interval", net_cfg["POLL_INTERVAL"])
    cfg.set("email", "email_enabled", net_cfg["EMAIL_ENABLED"])
    cfg.set("email", "email_max_rate", net_cfg["EMAIL_MAX_RATE"])
    cfg.set("email", "email_source", net_cfg["EMAIL_SOURCE"])
    cfg.set("email", "email_destination", net_cfg["EMAIL_DESTINATION"])
    cfg.set("email", "email_subject", net_cfg["EMAIL_SUBJECT"])
    cfg.set("email", "smtp_server", net_cfg["SMTP_SERVER"])
    cfg.set("syslog", "group_syslog_enabled", net_cfg["GROUP_SYSLOG_ENABLED"])
    cfg.set("syslog", "host_syslog_enabled", net_cfg["HOST_SYSLOG_ENABLED"])
    cfg.set("syslog", "syslog_server", net_cfg["SYSLOG_SERVER"])
    cfg.set("syslog", "syslog_port", net_cfg["SYSLOG_PORT"])
    cfg.set("syslog", "syslog_facility", net_cfg["SYSLOG_FACILITY"])
    filepath=os.path.join(CONFIG_FOLDER, CONFIG_FILE)
    try:
        fp=open(filepath, "wb")
        cfg.write(fp)
        fp.close()
    except:
        return 0
    return 1


def load_network():
    """Called from start_network to load initial network data"""
    global devices
    global net_cfg
    global HOST_STATS
    #
    filepath=os.path.join(net_cfg["NETWORK_DATA_FOLDER"], net_cfg["NETWORK_DATA_FILE"])
    #
    # Loads the network, returns 1 if ok, 0 if not
    if not os.path.exists(filepath):
        devices={0:[1,"0",0,"All Alerts","All Hosts showing alerts",0,0,0,"0"],
                 1:[0,"0",0,"Top","Skipole Monitor",0,0,0,"0"]}
        HOST_STATS=[0,0,0,0]
        return 1
    # So read the file
    fp=None
    try:
        fp=open(filepath,"r")
        u=cPickle.Unpickler(fp)
        devices=u.load()
        fp.close()
    except:
        if fp: fp.close()
        return 0
    # Having loaded the network file, make sure the status of all items is set to 0
    HOST_STATS=[0,0,0,0]
    host_count=0
    for id in network(0):
        set_net(id, STATUS, 0)
        set_net(id, OLDSTATUS, 0)
        if is_net_host(id):
            # Increment the host count
            host_count += 1
    HOST_STATS[0]=host_count
    # Perhaps should do more to check devices is a valid dictionary
    # i.e. could check that the 'last device' is actually equal to, or greater than the 'last device'
    # devices should now consist of a dictionary of all the pages and hosts
    return 1

# These functions are used to flag the network is being reloaded

def net_reload():
    """Return 1 if the network is being reloaded, 0 otherwise"""
    global NET_RELOAD
    if NET_RELOAD: return 1
    return 0

def set_net_reload():
    """Sets the NET_RELOAD flag, so net_reload() returns 1"""
    global NET_RELOAD
    NET_RELOAD = 1

def unset_net_reload():
    """Unsets the NET_RELOAD flag, so net_reload() returns 0"""
    global NET_RELOAD
    NET_RELOAD = 0
    # However if a network reload has just finished, this implies
    # a html refresh is required
    set_flag_html_refresh()


# These functions are used to flag the html should be refreshed

def flag_html_refresh():
    """Return 1 if the html pages should be refreshed, 0 otherwise"""
    global HTML_REFRESH
    if HTML_REFRESH: return 1
    return 0

def set_flag_html_refresh():
    """Sets the HTML_REFRESH flag, so flag_html_refresh returns 1"""
    global HTML_REFRESH
    HTML_REFRESH = 1

def unset_flag_html_refresh():
    """Unsets the HTML_REFRESH flag, so flag_html_refresh() returns 0"""
    global HTML_REFRESH
    HTML_REFRESH = 0


# These functions allow other modules to query or set the devices dictionary

def whole_network():
    """returns the dictionary of network devices"""
    global devices
    return devices

def get_net(id, parameter=3):
    """Returns the parameter value of item id"""
    global devices
    if id in devices:
        return devices[id][parameter]
    else:
        return -1

def is_net_exists(id):
    """Return true if id exists, false otherwise"""
    global devices
    return id in devices

def is_net_group(id):
    """Return 1 if id is a group, 0 otherwise"""
    global devices
    if devices[id][1] == "0":
        return 1
    else:
        return 0

def is_net_host(id):
    """Return 1 if id is a host, 0 otherwise"""
    global devices
    if devices[id][1] != "0":
        return 1
    else:
        return 0

def set_net(id, parameter, value):
    """Set the value of the parameter, of id"""
    global devices
    if id in devices:
        devices[id][parameter]=value
        return 1
    else:
        return 0

def add_net_item(id, value_list):
    """Check id is the next free id, appends the new device, returns 1 on success, 0 otherwise"""
    global devices
    # the id should be appended to the network
    if id != (devices[0][0] + 1): return 0
    devices[id]=value_list
    # and set the last id into devices[0][0]
    devices[0][0]= id
    return 1

def del_net_item(id):
    """Deletes id"""
    if id in devices:
        del devices[id]

def last_net():
    """Returns the id of the last device"""
    global devices
    return devices[0][0]

def network(id):
    """Network iterator, returns id from last item, decrementing to, but not including, this id"""
    global devices
    last_network_item=devices[0][0]
    for id in range(last_network_item, id, -1):
        if id in devices: yield id

# Functions to save, delete and load the network

def save_network():
    """Save the current network, to the current network data file"""
    global devices
    global net_cfg
    # Save devices, return 1 if successful, 0 if not
    # pathname where the network data is kept:
    filepath=os.path.join(net_cfg["NETWORK_DATA_FOLDER"], net_cfg["NETWORK_DATA_FILE"])
    if not os.path.exists(net_cfg["NETWORK_DATA_FOLDER"]):
        return 0
    # So write the file
    fp=None
    try:
        fp=open(filepath,"w")
        p=cPickle.Pickler(fp)
        p.dump(devices)
        fp.close()
    except:
        if fp: fp.close()
        return 0
    return 1

def delete_network():
    """Delete the current network, replacing it with the minimal alerts and top page"""
    global NET_RELOAD
    global HOST_STATS
    global HTML_REFRESH
    global devices
    NET_RELOAD=1
    #
    # Re-create new network
    devices={0:[1,"0",0,"All Alerts","All Hosts showing alerts",0,0,0,"0"],
             1:[0,"0",0,"Top","Skipole Monitor",0,0,0,"0"]}
    #
    HOST_STATS=[0,0,0,0]
    # Delete data and save the new minimal network to disc
    del_remake_data()
    #
    # Cause a network refresh
    HTML_REFRESH=1
    # And the jobs done
    NET_RELOAD=0
    return

def load_saved_network(newfile):
    """Load a previously saved network from newfile"""
    global NET_RELOAD
    global HOST_STATS
    global devices
    if not os.path.exists(newfile):return
    NET_RELOAD=1
    #
    # Load in new network
    fp=None
    try:
        fp=open(newfile,"r")
        u=cPickle.Unpickler(fp)
        devices=u.load()
        fp.close()
    except:
        if fp: fp.close()
        devices={0:[1,"0",0,"All Alerts","All Hosts showing alerts",0,0,0,"0"],
                 1:[0,"0",0,"Top","Skipole Monitor",0,0,0,"0"]}
    #
    # Having loaded the network file, make sure the status is set to 0
    HOST_STATS=[0,0,0,0]
    host_count = 0
    for id in network(0):
        set_net(id, STATUS, 0)
        if is_net_host(id):
            # Increment the host count
            host_count += 1
    HOST_STATS[0]=host_count
    # Perhaps should do more to check devices is a valid dictionary
    # i.e. could check that the 'last device' is actually equal to, or greater than the 'last device'
    # devices should now consist of a dictionary of all the pages and hosts
    #
    # Delete old data and save the new network to disc
    del_remake_data()
    #
    # Cause a network refresh
    HTML_REFRESH=1
    # And the jobs done
    NET_RELOAD=0
    return


# Function to read and import a csv file

def import_csv_network(filepath):
    """Load a csv network"""
    global NET_RELOAD
    global HOST_STATS
    #
    if not os.path.exists(filepath):return False, 0
    NET_RELOAD=1
    #
    # it reads the csv file, parses it and imports the hosts within
    # it returns a list of two objects
    # On success - True, Number of lines read
    # On failure - False, line number of failure
    #
    try:
        fp = open(filepath, "rb")
    except:
        NET_RELOAD=0
        return False, 0
    reader = csv.reader(fp)
    #
    # Store each row read
    row_store = []
    #
    try:
        for row in reader:
            # validate each line, then store it
            if not validate.parse_csv_row(row):
                fp.close()
                NET_RELOAD=0
                return False, int(reader.line_num)
            row_store.append(row)
    except csv.Error, e:
        fp.close()
        NET_RELOAD=0
        return False, int(reader.line_num)
    #
    if len(row_store) == 0:
        fp.close()
        NET_RELOAD=0
        return False, 0
    #
    # Having parsed the file, now add each item to the network
    for row in row_store:
        # Add item to the network
        add_csv_row(row)
    rows_read = len(row_store)
    # release memory
    del row_store
    fp.close()
    # Having drasticly changed the network, reset stats to zero
    HOST_STATS=[0,0,0,0]
    host_count = 0
    for id in network(0):
        set_net(id, STATUS, 0)
        if is_net_host(id):
            # Increment the host count
            host_count += 1
    HOST_STATS[0]=host_count
    #
    # Delete old data and save the new network to disc
    del_remake_data()
    #
    # Cause a network refresh
    HTML_REFRESH=1
    # And the jobs done
    NET_RELOAD=0
    return True, rows_read


def add_csv_row(row):
    """ Add the item defined by this row to the network, if necessary create a group for it."""
    new_network_item=last_net()+1
    #
    # Is there a group with the name?
    group_id = 0
    gname = row[0].strip()
    for id in network(0):
        # Checking groups only, if its a host, skip to next
        if is_net_group(id):
            # And find if a group has this name
            if get_net(id, NAME) ==  gname :
                # So gname does equal this group's name
                group_id = id
                # Add the host
                add_net_item(new_network_item, [id,row[1].strip(),0,row[2].strip(),row[3].strip(),0,0,0,"0"])
                new_network_item += 1
    #
    if group_id != 0:
        # A group must have been found, and the item added, so can return
        return
    #
    # If group_id is still zero, it means no group was found which matches this
    # group name - so create a group
    #
    add_net_item(new_network_item, [1,"0",0,gname,"New Group",0,0,0,"0"])
    group_id = new_network_item
    new_network_item += 1
    #
    # Add the host into this new group
    add_net_item(new_network_item, [group_id,row[1].strip(),0,row[2].strip(),row[3].strip(),0,0,0,"0"])
    return


